 /*******************************************************************************
  * Copyright (c) 2004, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.wizards.datatransfer;

 import java.io.FilterInputStream ;
 import java.io.IOException ;
 import java.io.InputStream ;

 /**
  * Input stream for reading files in ustar format (tar) compatible
  * with the specification in IEEE Std 1003.1-2001. Also supports
  * long filenames encoded using the GNU @LongLink extension.
  *
  * @since 3.1
  */
 public class TarInputStream extends FilterInputStream
 {
     private int nextEntry = 0;
     private int nextEOF = 0;
     private int filepos = 0;
     private int bytesread = 0;
     private TarEntry firstEntry = null;
     private String longLinkName = null;

     /**
      * Creates a new tar input stream on the given input stream.
      *
      * @param in input stream
      * @throws TarException
      * @throws IOException
      */
     public TarInputStream(InputStream in) throws TarException, IOException {
         super(in);

         // Read in the first TarEntry to make sure
 // the input is a valid tar file stream.
 firstEntry = getNextEntry();
     }

     /**
      * Create a new tar input stream, skipping ahead to the given entry
      * in the file.
      *
      * @param in input stream
      * @param entry skips to this entry in the file
      * @throws TarException
      * @throws IOException
      */
     TarInputStream(InputStream in, TarEntry entry) throws TarException, IOException {
         super(in);
         skipToEntry(entry);
     }

     /**
      * The checksum of a tar file header is simply the sum of the bytes in
      * the header.
      *
      * @param header
      * @return checksum
      */
     private long headerChecksum(byte[] header) {
         long sum = 0;
         for(int i = 0; i < 512; i++) {
             sum += header[i] & 0xff;
         }
         return sum;
     }

     /**
      * Skips ahead to the position of the given entry in the file.
      *
      * @param entry
      * @returns false if the entry has already been passed
      * @throws TarException
      * @throws IOException
      */
     boolean skipToEntry(TarEntry entry) throws TarException, IOException {
         int bytestoskip = entry.filepos - bytesread;
         if(bytestoskip < 0) {
             return false;
         }
         while(bytestoskip > 0) {
             long ret = in.skip(bytestoskip);
             if(ret < 0) {
                 throw new IOException ("early end of stream"); //$NON-NLS-1$
 }
             bytestoskip -= ret;
             bytesread += ret;
         }
         filepos = entry.filepos;
         nextEntry = 0;
         nextEOF = 0;
         // Read next header to seek to file data.
 getNextEntry();
         return true;
     }

     /**
      * Returns true if the header checksum is correct.
      *
      * @param header
      * @return true if this header has a valid checksum
      */
     private boolean isValidTarHeader(byte[] header) {
         long fileChecksum, calculatedChecksum;
         int pos, i;
         
         pos = 148;
         StringBuffer checksumString = new StringBuffer ();
         for(i = 0; i < 8; i++) {
             if(header[pos + i] == ' ') {
                 continue;
             }
             if(header[pos + i] == 0 || !Character.isDigit((char) header[pos + i])) {
                 break;
             }
             checksumString.append((char) header[pos + i]);
         }
         if(checksumString.length() == 0) {
             return false;
         }
         if(checksumString.charAt(0) != '0') {
             checksumString.insert(0, '0');
         }
         try {
             fileChecksum = Long.decode(checksumString.toString()).longValue();
         } catch(NumberFormatException exception) {
             //This is not valid if it cannot be parsed
 return false;
         }

         // Blank out the checksum.
 for(i = 0; i < 8; i++) {
             header[pos + i] = ' ';
         }
         calculatedChecksum = headerChecksum(header);

         return (fileChecksum == calculatedChecksum);
     }

     /**
      * Returns the next entry in the tar file. Does not handle
      * GNU @LongLink extensions.
      *
      * @return the next entry in the tar file
      * @throws TarException
      * @throws IOException
      */
     TarEntry getNextEntryInternal() throws TarException, IOException {
         byte[] header = new byte[512];
         int pos = 0;
         int i;

         if(firstEntry != null) {
             TarEntry entryReturn = firstEntry;
             firstEntry = null;
             return entryReturn;
         }

         while(nextEntry > 0) {
             long ret = in.skip(nextEntry);
             if(ret < 0) {
                 throw new IOException ("early end of stream"); //$NON-NLS-1$
 }
             nextEntry -= ret;
             bytesread += ret;
         }

         int bytestoread = 512;
         while(bytestoread > 0) {
             int ret = super.read(header, 512 - bytestoread, bytestoread);
             if( ret < 0 ) {
                 throw new IOException ("early end of stream"); //$NON-NLS-1$
 }
             bytestoread -= ret;
             bytesread += ret;
         }

         // If we have a header of all zeros, this marks the end of the file.
 if(headerChecksum(header) == 0) {
             // We are at the end of the file.
 if(filepos > 0) {
                 return null;
             }
             
             // Invalid stream.
 throw new TarException("not in tar format"); //$NON-NLS-1$
 }
         
         // Validate checksum.
 if(!isValidTarHeader(header)) {
             throw new TarException("not in tar format"); //$NON-NLS-1$
 }

         while (pos < 100 && header[pos] != 0) {
             pos++;
         }
         String name = new String (header, 0, pos, "UTF8"); //$NON-NLS-1$
 // Prepend the prefix here.
 pos = 345;
         if(header[pos] != 0) {
             while (pos < 500 && header[pos] != 0) {
                 pos++;
             }
             String prefix = new String (header, 345, pos - 345, "UTF8"); //$NON-NLS-1$
 name = prefix + "/" + name; //$NON-NLS-1$
 }
         
         TarEntry entry;
         if(longLinkName != null) {
             entry = new TarEntry(longLinkName, filepos);
             longLinkName = null;
         } else {
             entry = new TarEntry(name, filepos);
         }
         if(header[156] != 0) {
             entry.setFileType(header[156]);
         }
         
         pos = 100;
         StringBuffer mode = new StringBuffer ();
         for(i = 0; i < 8; i++) {
             if(header[pos + i] == 0) {
                 break;
             }
             if(header[pos + i] == ' ') {
                 continue;
             }
             mode.append((char) header[pos + i]);
         }
         if(mode.length() > 0 && mode.charAt(0) != '0') {
             mode.insert(0, '0');
         }
         try {
             long fileMode = Long.decode(mode.toString()).longValue();
             entry.setMode(fileMode);
         } catch(NumberFormatException nfe) {
             throw new TarException(DataTransferMessages.TarImport_invalid_tar_format, nfe);
         }
         
         pos = 100 + 24;
         StringBuffer size = new StringBuffer ();
         for(i = 0; i < 12; i++) {
             if(header[pos + i] == 0) {
                 break;
             }
             if(header[pos + i] == ' ') {
                 continue;
             }
             size.append((char) header[pos + i]);
         }
         if(size.charAt(0) != '0') {
             size.insert(0, '0');
         }
         int fileSize;
         try {
             fileSize = Integer.decode(size.toString()).intValue();
         } catch(NumberFormatException nfe) {
             throw new TarException(DataTransferMessages.TarImport_invalid_tar_format, nfe);
         }

         entry.setSize(fileSize);
         nextEOF = fileSize;
         if(fileSize % 512 > 0) {
             nextEntry = fileSize + (512 - (fileSize % 512));
         } else {
             nextEntry = fileSize;
         }
         filepos += (nextEntry + 512);
         return entry;
     }

     /**
      * Moves ahead to the next file in the tar archive and returns
      * a TarEntry object describing it.
      *
      * @return the next entry in the tar file
      * @throws TarException
      * @throws IOException
      */
     public TarEntry getNextEntry() throws TarException, IOException {
         TarEntry entry = getNextEntryInternal();

         if(entry != null && entry.getName().equals("././@LongLink")) { //$NON-NLS-1$
 // This is a GNU extension for doing long filenames.
 // We get a file called ././@LongLink which just contains
 // the real pathname.
 byte[] longNameData = new byte[(int) entry.getSize()];
             int bytesread = 0;
             while (bytesread < longNameData.length) {
                 int cur = read(longNameData, bytesread, longNameData.length - bytesread);
                 if (cur < 0) {
                     throw new IOException ("early end of stream"); //$NON-NLS-1$
 }
                 bytesread += cur;
             }

             int pos = 0;
             while (pos < longNameData.length && longNameData[pos] != 0) {
                 pos++;
             }
             longLinkName = new String (longNameData, 0, pos, "UTF8"); //$NON-NLS-1$
 return getNextEntryInternal();
         }
         return entry;
     }

     /* (non-Javadoc)
      * @see java.io.FilterInputStream#read(byte[], int, int)
      */
     public int read(byte[] b, int off, int len) throws IOException {
         if(nextEOF == 0) {
             return -1;
         }
         if(len > nextEOF) {
             len = nextEOF;
         }
         int size = super.read(b, off, len);
         nextEntry -= size;
         nextEOF -= size;
         bytesread += size;
         return size;
     }

     /* (non-Javadoc)
      * @see java.io.FilterInputStream#read()
      */
     public int read() throws IOException {
         byte[] data = new byte[1];
         int size = read(data, 0, 1);
         if (size < 0) {
             return size;
         }
         return data[0];
     }
 }

